﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace SpriteSheetsFabric.Wpf
{
    public partial class Baker
    {
        private int i = 0;
        private bool isCompleted = false;
        private readonly Queue<BakeItem> itemsQueue = new Queue<BakeItem>();
        private BakeHandler completedCallBackHandler = null;
        private FrameworkElement currentTargetSession = null;

        /// <summary>
        /// Initializes a new instance of the <see cref="Baker"/> class.
        /// </summary>
        private Baker(FrameworkElement target)
        {
            this.currentTargetSession = target;
        }

        /// <summary>
        /// Creates a baker for storyboard.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="storyboardName">The name of the sto.</param>
        /// <param name="duration">The duration.</param>
        /// <returns></returns>
        public static Baker CreateOnElementForStoryboard(FrameworkElement target, string storyboardName, Duration duration)
        {
            return new Baker(target).Queue(new BakeItem(target, storyboardName, duration));
        }

        /// <summary>
        /// Creates a baker for storyboard.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="name">The name.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="storyboard">The storyboard.</param>
        /// <returns></returns>
        public static Baker CreateOnElementForStoryboard(FrameworkElement target, string name, Duration duration, Storyboard storyboard)
        {
            return new Baker(target).Queue(new BakeItem(target, name, duration, storyboard));
        }

        /// <summary>
        /// Ands the specified storyboard name.
        /// </summary>
        /// <param name="storyboardName">Name of the storyboard.</param>
        /// <param name="duration">The duration.</param>
        /// <returns></returns>
        public Baker And(string storyboardName, Duration duration)
        {
            this.CheckUse();
            return this.Queue(new BakeItem(currentTargetSession, storyboardName, duration));
        }

        /// <summary>
        /// Ands the specified name.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="storyboard">The storyboard.</param>
        /// <returns></returns>
        public Baker And(string name, Duration duration, Storyboard storyboard)
        {
            this.CheckUse();
            return this.Queue(new BakeItem(currentTargetSession, name, duration, storyboard));
        }

        /// <summary>
        /// Ands the specified storyboard name.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="storyboardName">Name of the storyboard.</param>
        /// <param name="duration">The duration.</param>
        /// <returns></returns>
        public Baker AndForAnotherElement(FrameworkElement target, string storyboardName, Duration duration)
        {
            this.CheckUse();
            this.currentTargetSession = target;
            return this.Queue(new BakeItem(target, storyboardName, duration));
        }

        /// <summary>
        /// Ands the specified name.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="name">The name.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="storyboard">The storyboard.</param>
        /// <returns></returns>
        public Baker AndForAnotherElement(FrameworkElement target, string name, Duration duration, Storyboard storyboard)
        {
            this.CheckUse();
            this.currentTargetSession = target;
            return this.Queue(new BakeItem(target, name, duration, storyboard));
        }

        /// <summary>
        /// Bakes this instance.
        /// </summary>
        public BakeHandler Bake()
        {
            this.CheckUse();
            this.DoUnitBake(itemsQueue.Dequeue());
            return completedCallBackHandler = new BakeHandler();
        }

        /// <summary>
        /// Checks the use.
        /// </summary>
        private void CheckUse()
        {
            if(this.isCompleted)
            {
                throw new InvalidOperationException("You cannot execute the baker more than one time. Please create another one to bake.");
            }
        }

        /// <summary>
        /// Queues the specified bake item.
        /// </summary>
        /// <param name="bakeItem">The bake item.</param>
        /// <returns></returns>
        private Baker Queue(BakeItem bakeItem)
        {
            this.itemsQueue.Enqueue(bakeItem);
            return this;
        }

        /// <summary>
        /// Does the baking for one bakeItem.
        /// </summary>
        /// <param name="bakeItem">The bake item.</param>
        private void DoUnitBake(BakeItem bakeItem)
        {
            this.DoUnitBake(bakeItem.Target, bakeItem.Name, bakeItem.Duration, bakeItem.Storyboard ?? (Storyboard)bakeItem.Target.FindResource(bakeItem.Name));
        }

        /// <summary>
        /// Does the baking for one bakeItem.
        /// </summary>
        /// <param name="target">The target.</param>
        /// <param name="name">The name.</param>
        /// <param name="duration">The duration.</param>
        /// <param name="storyboard">The storyboard.</param>
        private void DoUnitBake(FrameworkElement target, string name, Duration duration, Storyboard storyboard)
        {
            if (Directory.Exists(name))
            {
                Directory.Delete(name, true);
            }

            Directory.CreateDirectory(name);

            //storyboard.FillBehavior = FillBehavior.Stop;
            storyboard.Begin();
            storyboard.Pause();

            i = 0;
            this.DispatchRender(target, storyboard, duration.TimeSpan.TotalMilliseconds, name);
        }

        private void DispatchRender(FrameworkElement target, Storyboard storyboard, double durationInMilliSeconds, string name)
        {
            storyboard.Seek(TimeSpan.FromMilliseconds(i * 1000d / 25d));
            target.Dispatcher.BeginInvoke((Action)(() =>
            {
                this.DoFrameRender(target, name);
                if (++i * 1000d < durationInMilliSeconds * 25d)
                {
                    this.DispatchRender(target, storyboard, durationInMilliSeconds, name);
                }
                else if(this.itemsQueue.Count > 0)
                {
                    // This is the only way I found to stop the handoff of the storyboard at the moment ...
                    storyboard.Completed += (sende, e) => this.DoUnitBake(this.itemsQueue.Dequeue());
                    storyboard.Begin();
                }
                else
                {
                    storyboard.Completed += (sende, e) => this.completedCallBackHandler.Invoke();
                    storyboard.Begin();
                    this.isCompleted = true;
                }
            }), DispatcherPriority.Loaded);
        }

        private void DoFrameRender(FrameworkElement target, string name)
        {
            target.UpdateLayout();
            var bitmap = target.RenderIntoBitmap(new Rect(0, 0, target.Width, target.Height));
            using (var writer = new StreamWriter(string.Format("{1}/{1}_{0}.bmp", i, name)))
            {
                BitmapEncoder encoder = new BmpBitmapEncoder();
                encoder.Frames.Add(BitmapFrame.Create(bitmap));
                encoder.Save(writer.BaseStream);
            }
        }
    }
}
